﻿using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using VXCompany.SilverlightLogging.MVVM.Models.Stores;
using VXCompany.SilverlightLogging.MVVM.Models.SyncBehaviors;
using VXCompany.SilverlightLogging.ServiceProxy;
namespace VXCompany.SilverlightLogging.MVVM.Models
{
	/// <summary>
	/// Handles client exceptions. Created by LoggingApplicationService.
	/// </summary>
	/// <example>
	/// To configure logging on WP7, add this code to App.xaml:
	/// 1. Add namespaces:
	/// xmlns:svc="clr-namespace:VXCompany.SilverlightLogging;assembly=VXCompany.SilverlightLogging.WP7"
	/// xmlns:str="clr-namespace:VXCompany.SilverlightLogging.MVVM.Models.Stores;assembly=VXCompany.SilverlightLogging.WP7"
	/// 
	/// 2. Add ApplicationLifetimeObject:
	/// <Application.ApplicationLifetimeObjects>
	///		<svc:LoggingApplicationService ApplicationName="WP7 Test UI">
	/// 		<svc:LoggingApplicationService.Store>
	/// 			<str:ApplicationStore />	
	/// 		</svc:LoggingApplicationService.Store>
	///		</svc:LoggingApplicationService>
	/// </Application.ApplicationLifetimeObjects>
	/// 
	/// To configure logging on SL4, add this code to App.xaml:
	/// 1. Add namespaces:
	/// xmlns:svc="clr-namespace:VXCompany.SilverlightLogging;assembly=VXCompany.SilverlightLogging.SL4"
	/// xmlns:str="clr-namespace:VXCompany.SilverlightLogging.MVVM.Models.Stores;assembly=VXCompany.SilverlightLogging.SL4"
	/// 
	/// 2. Add ApplicationLifetimeObject:
	/// <Application.ApplicationLifetimeObjects>
	///		<svc:LoggingApplicationService ApplicationName="SL4 Test UI">
	/// 		<svc:LoggingApplicationService.Store>
	/// 			<str:ApplicationStore />
	/// 		</svc:LoggingApplicationService.Store>
	///		</svc:LoggingApplicationService>
	/// </Application.ApplicationLifetimeObjects>
	/// 
	/// ApplicationName can be specified, and you can explicitly create a temporary store for Exceptions. Available stores are:
	/// 1. ApplicationStore, which uses IsolatedStorageSettings.ApplicationSettings to store errors in.
	/// 2. IsoStreamStore, which uses IsolatedStorageFileStream to store errors in.
	/// </example>
	public class ExceptionPolicy : NotifyingBase
	{
		#region Static Members

		/// <summary>
		/// Convenience property that returns LoggingApplicationService.Current.ExceptionPolicy. (provided that the service is added to App.xaml)
		/// </summary>
		public static ExceptionPolicy Current
		{
			get
			{
				if (LoggingApplicationService.Current != null)
				{
					return LoggingApplicationService.Current.ExceptionPolicy;
				}
				return null;
			}
		}

		#endregion Static Members

		private ServiceHelper _ServiceHelper;
		private IStore _Store;
		private LoggingServiceClient _LoggingServiceClient;
		private string _ApplicationName;
		private ObservableCollection<ISyncBehavior> _RunningSyncBehaviors;
		private LoggingConfiguration _LoggingConfiguration;
		private bool _IsServiceAvailable;
		private bool _IsSynchronizing;

		#region Properties

		/// <summary>
		/// Gets the exception store.
		/// </summary>
		public IStore Store
		{
			get { return _Store; }
			private set
			{
				_Store = value;
				RaisePropertyChanged("Store");
			}
		}

		/// <summary>
		/// Gets LoggingServiceClient (using the ILoggingServiceFactory if it is available)
		/// </summary>		
		public LoggingServiceClient LoggingServiceClient
		{
			get
			{
				if (LoggingServiceFactory != null)
				{
					LoggingServiceClient = LoggingServiceFactory.GetProxy();
				}
				return _LoggingServiceClient;
			}
			private set
			{
				if (_LoggingServiceClient == value)
					return;

				_LoggingServiceClient = value;
				RaisePropertyChanged("LoggingServiceClient");
			}
		}

		private ILoggingServiceClientFactory _LoggingServiceFactory;
		/// <summary>
		/// Gets or sets ILoggingServiceFactory implementation. 
		/// </summary>
		public ILoggingServiceClientFactory LoggingServiceFactory
		{
			get { return _LoggingServiceFactory; }
			set
			{
				if (value == _LoggingServiceFactory)
					return;

				_LoggingServiceFactory = value;
				RaisePropertyChanged("LoggingServiceFactory");
			}
		}
		

		/// <summary>
		/// Gets the configured logging settings.
		/// </summary>
		public LoggingConfiguration LoggingConfiguration
		{
			get { return _LoggingConfiguration; }
			private set
			{
				_LoggingConfiguration = value;
				RaisePropertyChanged("LoggingConfiguration");
			}
		}

		/// <summary>
		/// Gets or sets the application name.
		/// </summary>
		public string ApplicationName
		{
			get { return _ApplicationName; }
			set
			{
				_ApplicationName = value;
				RaisePropertyChanged("ApplicationName");
			}
		}

		/// <summary>
		/// Gets all SyncBehaviors.
		/// </summary>
		public ObservableCollection<ISyncBehavior> RunningSyncBehaviors
		{
			get { return _RunningSyncBehaviors; }
			private set
			{
				_RunningSyncBehaviors = value;
				RaisePropertyChanged("RunningSyncBehaviors");
			}
		}

		/// <summary>
		/// Indicates whether service is available.
		/// </summary>
		public bool IsServiceAvailable
		{
			get { return _IsServiceAvailable; }
			private set
			{
				_IsServiceAvailable = value;
				RaisePropertyChanged("IsServiceAvailable");
			}
		}


		/// <summary>
		/// Indicates whether a synchronization attempt is being made.
		/// </summary>
		public bool IsSynchronizing
		{
			get { return _IsSynchronizing; }
			private set
			{
				_IsSynchronizing = value;
				RaisePropertyChanged("IsSynchronizing");
			}
		}

		#endregion Properties

		/// <summary>
		/// Creates new instance, with default Store.
		/// </summary>		
		internal ExceptionPolicy()
		{
			//init using default store:
			IStore store = new ApplicationStore();			
			TryInitializeStore(ref store);			
			//catch unhandled errors
			Application.Current.UnhandledException += new EventHandler<ApplicationUnhandledExceptionEventArgs>(Current_UnhandledException);
		}

		/// <summary>
		/// Initializes logging policy Store with configured Store.
		/// </summary>
		public bool TryInitializeStore(ref IStore localStore)
		{
			bool isStoreOK = true;
			if (localStore == null)
			{
				throw new ArgumentNullException("localStore");
			}

			// trying to configure a store that doesn't work
			if (!localStore.IsStorageAvailable)
			{
				//log to memory as a fallback scenario.
				localStore = new MemoryStore();
				isStoreOK = false;
			}
			else
			{
				//transfer existing errors when switching Stores.
				if (Store != null
					&& Store.GetType() != localStore.GetType()
					&& Store.IsStorageAvailable
					)
				{
					foreach (var item in Store)
					{
						localStore.Add(item);
					}
					Store.Clear();
				}
			}

			Store = localStore;

			//notify admin
			if (Store is MemoryStore)
			{
				HandleException(new Exception("Using memory store at client."), ErrorLevel.Warning);
			}

			return isStoreOK;
		}

		/// <summary>
		/// Initialzes logging service client, using provided client, or the factory, or the configuration file (if both client and factory are null).
		/// </summary>
		/// <param name="client"></param>
		public void InitializeServiceClient(LoggingServiceClient client = null)
		{
			if (LoggingServiceFactory == null)
			{
				LoggingServiceClient = new LoggingServiceClient();
			}

			_ServiceHelper = new ServiceHelper(this);
			_ServiceHelper.ConfigurationUpdated += (sender, ea) =>
			{
				this.LoggingConfiguration = ea.Configuration;
				SetSyncBehavior();
			};
			_ServiceHelper.BeginGetConfig();

			_ServiceHelper.PropertyChanged += (sender, ea) =>
			{
				if (ea.PropertyName == "IsServiceAvailable")
				{
					IsServiceAvailable = _ServiceHelper.IsServiceAvailable;
				}
				else if (ea.PropertyName == "IsSynchronizing")
				{
					IsSynchronizing = _ServiceHelper.IsSynchronizing;
				}
			};
		}

		/// <summary>
		/// Handles the provided client exception.
		/// </summary>
		/// <remarks>To be logged, the exception must be provided with a level higher than or equal to the MinLogLevel in the configuration.</remarks>
		/// <param name="exception"></param>
		public void HandleException(Exception exception, ErrorLevel level = ErrorLevel.Critical)
		{
			if (Store != null
				&& exception != null
				&& level >= LoggingConfiguration.MinLogLevel)
			{
				var exceptionMessage = MapExceptionToError(exception, level);
				Store.Add(exceptionMessage);
			}
		}
		
		/// <summary>
		/// Handles unhandled exceptions.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void Current_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
		{
			HandleException(e.ExceptionObject, ErrorLevel.Critical);
		}

		/// <summary>
		/// Sets Sync behavior based on configuration.
		/// </summary>
		private void SetSyncBehavior()
		{
			if (RunningSyncBehaviors == null)
			{
				RunningSyncBehaviors = new ObservableCollection<ISyncBehavior>();
			}
			else
			{
				if (RunningSyncBehaviors.Count > 0)
				{
					RunningSyncBehaviors.Cast<IInternalSyncBehavior>().ForEach(b => b.Stop());
					RunningSyncBehaviors.Clear();
				}
			}

			if (SyncBehaviorFlags.AppCrash == (LoggingConfiguration.SyncBehaviorFlags & SyncBehaviorFlags.AppCrash))
			{
				RunningSyncBehaviors.Add(new SyncBehaviors.AppCrashSync());
			}
			if (SyncBehaviorFlags.AppStart == (LoggingConfiguration.SyncBehaviorFlags & SyncBehaviorFlags.AppStart))
			{
				RunningSyncBehaviors.Add(new SyncBehaviors.AppStartSync());
			}
			if (SyncBehaviorFlags.AppEnd == (LoggingConfiguration.SyncBehaviorFlags & SyncBehaviorFlags.AppEnd))
			{
				RunningSyncBehaviors.Add(new SyncBehaviors.AppEndSync());
			}
			if (SyncBehaviorFlags.PerMinute == (LoggingConfiguration.SyncBehaviorFlags & SyncBehaviorFlags.PerMinute))
			{
				RunningSyncBehaviors.Add(new SyncBehaviors.PeriodicSync(TimeSpan.FromMinutes(1)));
			}
			if (SyncBehaviorFlags.PerTwoMinutes == (LoggingConfiguration.SyncBehaviorFlags & SyncBehaviorFlags.PerTwoMinutes))
			{
				RunningSyncBehaviors.Add(new SyncBehaviors.PeriodicSync(TimeSpan.FromMinutes(2)));
			}
			if (SyncBehaviorFlags.PerFiveMinutes == (LoggingConfiguration.SyncBehaviorFlags & SyncBehaviorFlags.PerFiveMinutes))
			{
				RunningSyncBehaviors.Add(new SyncBehaviors.PeriodicSync(TimeSpan.FromMinutes(5)));
			}
			if (SyncBehaviorFlags.PerTenMinutes == (LoggingConfiguration.SyncBehaviorFlags & SyncBehaviorFlags.PerTenMinutes))
			{
				RunningSyncBehaviors.Add(new SyncBehaviors.PeriodicSync(TimeSpan.FromMinutes(10)));
			}
			if (SyncBehaviorFlags.PerFifteenMinutes == (LoggingConfiguration.SyncBehaviorFlags & SyncBehaviorFlags.PerFifteenMinutes))
			{
				RunningSyncBehaviors.Add(new SyncBehaviors.PeriodicSync(TimeSpan.FromMinutes(15)));
			}

			////To test failing behavior, enable the line below.
			//RunningSyncBehaviors.Add(new TestSvcFailureBehavior());

			//Configure behaviors.
			ConfigureSyncBehaviors();
		}

		/// <summary>
		/// Configures all behaviors in RunningSyncBehaviors.
		/// </summary>
		private void ConfigureSyncBehaviors()
		{			
			foreach (var item in RunningSyncBehaviors.Cast<IInternalSyncBehavior>())
			{
				_ServiceHelper.ExceptionsSynced += (sender, ea) =>
				{
					EndSynchronizeAll(ea.Result);
				};
				item.SyncAction = () => { if (_ServiceHelper.IsServiceAvailable) _ServiceHelper.BeginWrite(); };
				item.Start();
			}
		}

		/// <summary>
		/// Calls EndSynchronize on all active sync items.
		/// </summary>
		/// <param name="result"></param>
		private void EndSynchronizeAll(Result result)
		{
			foreach (var item in RunningSyncBehaviors.Cast<IInternalSyncBehavior>())
			{
				if (item.IsSynchronizing)
				{
					item.EndSynchronize(result);
				}
			}
		}

		/// <summary>
		/// Maps Exception to ExceptionMessage.
		/// </summary>
		/// <param name="exception"></param>
		/// <param name="level"></param>
		/// <returns></returns>
		private ExceptionMessage MapExceptionToError(Exception exception, ErrorLevel level = ErrorLevel.Critical)
		{
			string exceptionString = exception.ToString();

			return new ExceptionMessage
			{
				Message = exceptionString,
				Level = level,
				ClientApplication = ApplicationName,
				ClientAddress = LoggingConfiguration != null ? LoggingConfiguration.ClientAddress : "unknown address",
				UserID = LoggingConfiguration != null ? LoggingConfiguration.UserID : "unknown user",
				ExceptionMessageID = Guid.NewGuid(),
				DateTime = DateTime.Now.ToUniversalTime(),
				InnerException = exception.InnerException != null ? MapExceptionToError(exception.InnerException) : null,
			};
		}
	}
}
